package cn.tannn.ops.util;

import cn.tannn.ops.nginx.entity.CloudAccessKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.KeyPairUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.security.Security;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Supplier;

/**
 * 域名证书获取工具
 *
 * @author <a href="https://t.tannn.cn/">tan</a>
 * @version V1.0
 * @date 2024/8/29 上午11:24
 * @see <a href="https://shredzone.org/maven/acme4j/ca/index.html">acme4j</a>
 * @see <a href="https://shredzone.org/maven/acme4j/example.html">acme4j example</a>
 * @see <a href="https://api.aliyun.com/api-tools/demo/Alidns/87dd2c5a-bb87-4269-8bb1-6e021be52123">阿里sdk</a>
 */
public class AcmeUtil {

    private static final Logger LOG = LoggerFactory.getLogger(AcmeUtil.class);

    //  ACME 服务器 https://shredzone.org/maven/acme4j/ca/index.html
    // Let's Encrypt
    // Pebble
    // SSL.com
    // ZeroSSL  "acme://zerossl.com"
    private static final String CA_URI = "acme://letsencrypt.org";

    // A supplier for a new account KeyPair. The default creates a new EC key pair.
    private static Supplier<KeyPair> ACCOUNT_KEY_SUPPLIER = () -> KeyPairUtils.createKeyPair();

    // A supplier for a new domain KeyPair. The default creates a RSA key pair.
    private static Supplier<KeyPair> DOMAIN_KEY_SUPPLIER = () -> KeyPairUtils.createKeyPair(4096);

    // File name of the User Key Pair （ACME账号） - 无会自动注册
    private static final File USER_KEY_FILE = new File("user.key");

    // Maximum time to wait until VALID/INVALID is expected
    private static final Duration TIMEOUT = Duration.ofSeconds(240L);

    private enum ChallengeType {HTTP, DNS}

    public static void run(String domain, CloudAccessKey cloud) throws AcmeException, IOException, InterruptedException {
        Security.addProvider(new BouncyCastleProvider());
        AcmeUtil acmeUtil = new AcmeUtil();
        acmeUtil.fetchCertificate(domain, cloud);
    }


    /**
     * 自动注册 ACM服务 账号，然后给域名生成证书
     *
     * @param domain 域名
     * @param cloud  域名所属的云厂商信息
     */
    public void fetchCertificate(String domain, CloudAccessKey cloud) throws IOException, AcmeException, InterruptedException {
        // 加载ACM服务账号密钥对
        KeyPair userKeyPair = loadOrCreateUserKeyPair();

        // 创建一个ACM服务会话
        Session session = new Session(CA_URI);

        // 根据密钥对获取ACM账号
        Account acct = findOrRegisterAccount(session, userKeyPair);

        // 加载域名的密钥对 [同时会保存一个key文件，nginx要用]
        KeyPair domainKeyPair = loadOrCreateDomainKeyPair(domain);

        // 为域名创建一个订单
        Order order = acct.newOrder().domains(domain).create();

        // 验证对域名的控制权
        /// https://shredzone.org/maven/acme4j/ca/index.html
        for (Authorization auth : order.getAuthorizations()) {
            authorize(auth, cloud);
        }

        // 请求证书
        // Wait for the order to become READY
        order.waitUntilReady(TIMEOUT);

        // Order the certificate
        order.execute(domainKeyPair);

        // Wait for the order to complete
        Status status = order.waitForCompletion(TIMEOUT);
        if (status != Status.VALID) {
            LOG.error("Order has failed, reason: {}", order.getError()
                    .map(Problem::toString)
                    .orElse("unknown"));
            throw new AcmeException("Order failed... Giving up.");
        }

        // 下载证书
        // Get the certificate
        Certificate certificate = order.getCertificate();

        LOG.info("Success! The certificate for domains {} has been generated!", domain);
        LOG.info("Certificate URL: {}", certificate.getLocation());

        // 保存证书 nginx pem
        try (FileWriter fw = new FileWriter(new File(domain + ".pem"))) {
            certificate.writeCertificate(fw);
        }

    }


    /**
     * 加载 ACME 服务账号密钥对
     * <p>
     * 从 {@link #USER_KEY_FILE}. 加载一个用户密钥对。如果文件不存在则生成并保存新的密钥对
     * <p>
     * 把这对钥匙放在安全的地方!在生产环境中，如果丢失密钥对，将无法再次访问您的帐户。
     *
     * @return User's {@link KeyPair}.
     */
    private KeyPair loadOrCreateUserKeyPair() throws IOException {
        if (USER_KEY_FILE.exists()) {
            // 如果有密钥文件，读取它
            try (FileReader fr = new FileReader(USER_KEY_FILE)) {
                return KeyPairUtils.readKeyPair(fr);
            }

        } else {
            // 如果没有，则创建一个新的密钥对并保存
            KeyPair userKeyPair = ACCOUNT_KEY_SUPPLIER.get();
            try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {
                KeyPairUtils.writeKeyPair(userKeyPair, fw);
            }
            return userKeyPair;
        }
    }

    /**
     * 加载： Nginx证书文件 【domain-chain.key】
     * <p>
     * 加载域密钥对。如果该文件不存在，则生成一个新的密钥对并保存。
     *
     * @param domain 域名，用来给key文件命名的
     * @return Domain {@link KeyPair}.
     */
    private KeyPair loadOrCreateDomainKeyPair(String domain) throws IOException {
        File domainKeyFile = new File(domain + ".key");
        if (domainKeyFile.exists()) {
            try (FileReader fr = new FileReader(domainKeyFile)) {
                return KeyPairUtils.readKeyPair(fr);
            }
        } else {
            KeyPair domainKeyPair = DOMAIN_KEY_SUPPLIER.get();
            try (FileWriter fw = new FileWriter(domainKeyFile)) {
                KeyPairUtils.writeKeyPair(domainKeyPair, fw);
            }
            return domainKeyPair;
        }
    }

    /**
     * 授权域。它将与帐户相关联，因此您稍后将能够为该域检索已签名的证书。
     * <p>
     * 只支持 DNS认证
     *
     * @param auth  {@link Authorization} to perform
     * @param cloud 域名所属的云厂商信息
     */
    private void authorize(Authorization auth, CloudAccessKey cloud) throws AcmeException, InterruptedException {
        LOG.info("Authorization for domain {}", auth.getIdentifier().getDomain());

        // The authorization is already valid. No need to process a challenge.
        if (auth.getStatus() == Status.VALID) {
            return;
        }

        // Find the desired challenge and prepare it.
        Dns01Challenge challenge = dnsChallenge(auth);

        // 这里去云服务商添加一条 txt 解析记录, 验证域名所有权.
        String domainName = auth.getIdentifier().getDomain();
        String value = challenge.getDigest();
        DnsUtil dnsUtil = new DnsUtil();
        dnsUtil.addDomainRecord_SSL(domainName, auth, value, cloud);

        // 如果已经得到验证，则不需要再次执行它。
        if (challenge.getStatus() == Status.VALID) {
            return;
        }

        // 开始认证
        challenge.trigger();

        // 为完成认证进行投票。
        Status status = challenge.waitForCompletion(TIMEOUT);
        if (status != Status.VALID) {
            LOG.error("Challenge has failed, reason: {}", challenge.getError()
                    .map(Problem::toString)
                    .orElse("unknown"));
            // 删除添加的记录
            dnsUtil.deleteDomainRecord();
            throw new AcmeException("Challenge failed... Giving up.");

        }
        // After challenge completion
        // 删除添加的记录
        dnsUtil.deleteDomainRecord();

        LOG.info("Challenge has been completed. Remember to remove the validation resource.");
//        completeChallenge("Challenge has been completed.\nYou can remove the resource again now.");
    }

    /**
     * DNS 认证信息创建
     *
     * @param auth {@link Authorization} to find the challenge in
     * @return {@link Challenge} to verify
     */
    private Dns01Challenge dnsChallenge(Authorization auth) throws AcmeException {
        // Find a single dns-01 challenge
        Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE)
                .map(Dns01Challenge.class::cast)
                .orElseThrow(() -> new AcmeException("Found no " + Dns01Challenge.TYPE
                        + " challenge, don't know what to do..."));
        StringBuilder message = new StringBuilder();
        message.append("Please create a TXT record:\n\n");
        message.append(Dns01Challenge.toRRName(auth.getIdentifier()))
                .append(" IN TXT ")
                .append(challenge.getDigest());
//        acceptChallenge(message.toString());
        // todo 这里去云服务商添加一条 txt 解析记录, 验证域名所有权.
        return challenge;
    }

    /**
     * 提供准备挑战验证的说明，并等待解散。如果用户取消了对话框，则抛出异常。
     *
     * @param message 要在对话框中显示的说明
     */
    private void acceptChallenge(String message) throws AcmeException {
        int option = JOptionPane.showConfirmDialog(null,
                message,
                "Prepare Challenge",
                JOptionPane.OK_CANCEL_OPTION);
        if (option == JOptionPane.CANCEL_OPTION) {
            throw new AcmeException("User cancelled the challenge");
        }
    }


    /**
     * 在ACME服务器上找到您的{@link Account}。它将通过用户的公钥找到。如果服务器还不知道您的密钥，则将创建一个新帐户。
     * <p>
     * 这是一个查找{@link Account}的简单方法。
     * 更好的方法是使用{@link Account#getLocation()}获取新帐户的URL并将其存储在某个地方。
     * 如果您以后需要访问您的帐户，请使用存储的位置通过{@link Session#login(URL, KeyPair)}重新连接到它。
     *
     * @param session {@link Session} to bind with
     * @return {@link Account}
     */
    private Account findOrRegisterAccount(Session session, KeyPair accountKey) throws AcmeException {
        // Ask the user to accept the TOS, if server provides us with a link.
        Optional<URI> tos = session.getMetadata().getTermsOfService();
//        if (tos.isPresent()) {
//            acceptAgreement(tos.get());
//        }
        AccountBuilder accountBuilder = new AccountBuilder()
                .agreeToTermsOfService()
                .useKeyPair(accountKey);

        Account account = accountBuilder.create(session);
        LOG.info("Registered a new user, URL: {}", account.getLocation());

        return account;
    }


    /**
     * 向用户显示服务条款的链接，并要求用户确认。如果用户拒绝确认，则抛出异常。
     *
     * @param agreement {@link URI} of the Terms of Service
     */
    private void acceptAgreement(URI agreement) throws AcmeException {
        int option = JOptionPane.showConfirmDialog(null,
                "Do you accept the Terms of Service?\n\n" + agreement,
                "Accept ToS",
                JOptionPane.YES_NO_OPTION);
        if (option == JOptionPane.NO_OPTION) {
            throw new AcmeException("User did not accept Terms of Service");
        }
    }

    /**
     * 显示删除质询验证的说明，并等待解除
     *
     * @param message 要在对话框中显示的说明
     */
    private void completeChallenge(String message) {
        JOptionPane.showMessageDialog(null,
                message,
                "Complete Challenge",
                JOptionPane.INFORMATION_MESSAGE);
    }

    /**
     * Prepares a HTTP challenge. (我不支持)
     * <p>
     * The verification of this challenge expects a file with a certain content to be
     * reachable at a given path under the domain to be tested.
     * <p>
     * This example outputs instructions that need to be executed manually. In a
     * production environment, you would rather generate this file automatically, or maybe
     * use a servlet that returns {@link Http01Challenge#getAuthorization()}.
     *
     * @param auth {@link Authorization} to find the challenge in
     * @return {@link Challenge} to verify
     */
    private Challenge httpChallenge(Authorization auth) throws AcmeException {
        // Find a single http-01 challenge
        Http01Challenge challenge = auth.findChallenge(Http01Challenge.class)
                .orElseThrow(() -> new AcmeException("Found no " + Http01Challenge.TYPE
                        + " challenge, don't know what to do..."));

        // Output the challenge, wait for acknowledge...
        LOG.info("Please create a file in your web server's base directory.");
        LOG.info("It must be reachable at: http://{}/.well-known/acme-challenge/{}",
                auth.getIdentifier().getDomain(), challenge.getToken());
        LOG.info("File name: {}", challenge.getToken());
        LOG.info("Content: {}", challenge.getAuthorization());
        LOG.info("The file must not contain any leading or trailing whitespaces or line breaks!");
        LOG.info("If you're ready, dismiss the dialog...");

        StringBuilder message = new StringBuilder();
        message.append("Please create a file in your web server's base directory.\n\n");
        message.append("http://")
                .append(auth.getIdentifier().getDomain())
                .append("/.well-known/acme-challenge/")
                .append(challenge.getToken())
                .append("\n\n");
        message.append("Content:\n\n");
        message.append(challenge.getAuthorization());
//        acceptChallenge(message.toString());
        // todo https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E#1-http-%E6%96%B9%E5%BC%8F%E9%9C%80%E8%A6%81%E5%9C%A8%E4%BD%A0%E7%9A%84%E7%BD%91%E7%AB%99%E6%A0%B9%E7%9B%AE%E5%BD%95%E4%B8%8B%E6%94%BE%E7%BD%AE%E4%B8%80%E4%B8%AA%E6%96%87%E4%BB%B6-%E6%9D%A5%E9%AA%8C%E8%AF%81%E4%BD%A0%E7%9A%84%E5%9F%9F%E5%90%8D%E6%89%80%E6%9C%89%E6%9D%83%E5%AE%8C%E6%88%90%E9%AA%8C%E8%AF%81-%E7%84%B6%E5%90%8E%E5%B0%B1%E5%8F%AF%E4%BB%A5%E7%94%9F%E6%88%90%E8%AF%81%E4%B9%A6%E4%BA%86
        return challenge;
    }

}
